🔒 Sicurezza in Python

Una guida completa alla sicurezza applicativa: hashing, crittografia, protezione da attacchi e best practices

🛡️ Introduzione alla Sicurezza Applicativa

⚠️ Perché la Sicurezza è Fondamentale?

La sicurezza non è un optional, ma un requisito fondamentale di ogni applicazione moderna. Pensa alla sicurezza come alle fondamenta di una casa: se sono deboli, l'intera struttura è a rischio, non importa quanto sia bella l'architettura sopra.

Quando sviluppiamo un'applicazione, dobbiamo proteggere:

  • 🔐 I dati degli utenti: password, informazioni personali, dati sensibili
  • 💾 I dati dell'applicazione: database, file, configurazioni
  • 🌐 Le comunicazioni: traffico di rete tra client e server
  • ⚙️ L'applicazione stessa: da injection, exploit, accessi non autorizzati

🚨 Conseguenze di una Scarsa Sicurezza

Un errore di sicurezza può avere conseguenze devastanti:

  • Furto di dati: password, carte di credito, dati personali degli utenti
  • Perdita di fiducia: gli utenti non torneranno dopo un data breach
  • Sanzioni legali: GDPR e altre normative prevedono multe salate
  • Danni economici: costi di remediation, perdita di business, danni reputazionali
  • Compromissione del sistema: gli attaccanti possono prendere controllo completo

📋 Cosa Studieremo in Questa Lezione

Esploreremo i 5 pilastri fondamentali della sicurezza applicativa in Python:

  1. 🔐 Hashing e Password Security - Come proteggere le password usando bcrypt
  2. 🔑 Crittografia - Simmetrica e asimmetrica per proteggere i dati
  3. 💉 Prevenzione SQL Injection - Proteggere il database da attacchi
  4. ✅ Input Validation - Validare e sanitizzare i dati degli utenti
  5. 🌐 HTTPS e Certificati - Comunicazioni sicure su rete

🔐 1. Hashing e Password Security

🤔 Cos'è l'Hashing?

L'hashing è un processo matematico che trasforma un input (di qualsiasi lunghezza) in un output di lunghezza fissa, chiamato hash o digest. È come creare un'impronta digitale unica di un dato: sempre la stessa per lo stesso dato, ma impossibile da invertire.

📊 Processo di Hashing
Password
"Ciao123!"
Funzione Hash
(bcrypt)
Hash
$2b$12$KIXj...

⚠️ Il processo è unidirezionale: dall'hash non si può risalire alla password originale!

📌 Caratteristiche Fondamentali dell'Hashing

  • ✓ Deterministico: lo stesso input produce sempre lo stesso hash
  • ✓ Unidirezionale: impossibile (o estremamente difficile) risalire all'input dall'hash
  • ✓ Sensibile ai cambiamenti: anche una minima modifica dell'input produce un hash completamente diverso
  • ✓ Dimensione fissa: l'output ha sempre la stessa lunghezza, indipendentemente dall'input

⚠️ Hashing ≠ Crittografia

Attenzione! L'hashing e la crittografia sono due cose diverse:

Aspetto Hashing Crittografia
Reversibilità ❌ Unidirezionale (non reversibile) ✅ Bidirezionale (reversibile con chiave)
Uso principale Verifica integrità, password Protezione dati sensibili
Output Sempre stessa lunghezza Dipende dall'algoritmo
Esempio bcrypt, SHA-256 AES, RSA

🔑 Perché NON Salvare Password in Chiaro

❌ MAI Fare Questo

ERRORE GRAVISSIMO - Salvare le password in chiaro nel database:

# ❌ PERICOLOSISSIMO - NON FARE MAI!
class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password  # Password in chiaro!
    
    def check_password(self, password):
        return self.password == password  # Confronto diretto

# Nel database
# | ID | USERNAME | PASSWORD    |
# |----|----------|-------------|
# | 1  | mario    | MarioRossi1 |  ⚠️ Visibile a tutti!
# | 2  | anna     | Anna@2024   |  ⚠️ Visibile a tutti!

Problemi:

  • Se il database viene violato, tutte le password sono esposte
  • Chiunque abbia accesso al database (anche gli amministratori) può vedere le password
  • Gli utenti spesso usano la stessa password su più siti (credential stuffing)
  • Violazione GDPR e altre normative sulla privacy

✅ La Soluzione: Hashing con bcrypt

🎯 bcrypt: Lo Standard per le Password

bcrypt è un algoritmo di hashing progettato specificamente per le password. Include funzionalità avanzate che lo rendono estremamente sicuro:

  • Salt automatico: ogni password ha un "sale" casuale unico
  • Computazionalmente costoso: rende gli attacchi brute-force impraticabili
  • Configurabile: puoi aumentare il "costo" (rounds) per renderlo più lento
  • Resistente agli attacchi: rainbow tables inutili grazie al salt
🧂 Cos'è il Salt?

Il salt è una stringa casuale aggiunta alla password prima dell'hashing

❌ Senza Salt

"ciao123" → abc4b2a
"ciao123" → abc4b2a
Hash identici!
VS
✅ Con Salt

"ciao123" + aB3$ → $2b$12$7eX...
"ciao123" + 9p!Q → $2b$12$kN8...
Hash diversi!

💻 Installazione e Uso di bcrypt

pip install bcrypt

Esempio Base: Hashing di una Password

import bcrypt

# 1. Creare un hash da una password
password = "MiaSuperPassword123!"

# Convertire la stringa in bytes (bcrypt richiede bytes)
password_bytes = password.encode('utf-8')

# Generare il salt e creare l'hash
# Il numero 12 indica il "cost factor" (numero di rounds)
# Più alto = più sicuro ma più lento (valore tipico: 10-14)
hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt(rounds=12))

print(f"Password originale: {password}")
print(f"Hash generato: {hashed}")
# Output: b'$2b$12$KIXJLqvXhZ...' (sempre diverso ad ogni esecuzione!)

# 2. Verificare una password
# Simuliamo l'input dell'utente al login
input_password = "MiaSuperPassword123!"
input_bytes = input_password.encode('utf-8')

# Verifica se la password corrisponde all'hash
if bcrypt.checkpw(input_bytes, hashed):
    print("✅ Password corretta!")
else:
    print("❌ Password errata!")

# 3. Esempio con password sbagliata
wrong_password = "PasswordSbagliata"
wrong_bytes = wrong_password.encode('utf-8')

if bcrypt.checkpw(wrong_bytes, hashed):
    print("✅ Password corretta!")
else:
    print("❌ Password errata!")  # Questo verrà stampato

✅ Best Practices per Password Security

  1. Usa sempre bcrypt (o argon2, scrypt) per le password - mai MD5 o SHA-1
  2. Non salvare mai password in chiaro - nemmeno temporaneamente
  3. Usa rounds adeguati (10-14) - equilibrio tra sicurezza e performance
  4. Implementa rate limiting - blocca account dopo N tentativi falliti
  5. Richiedi password complesse - minimo 8 caratteri, mix di maiuscole/minuscole/numeri/simboli
  6. Usa HTTPS - le password devono viaggiare sempre su connessioni cifrate
  7. Implementa 2FA - autenticazione a due fattori per maggiore sicurezza
  8. Notifica cambi password - invia email quando la password viene modificata

📝 Quiz 1: Hashing e Password Security

Quale delle seguenti affermazioni sull'hashing è FALSA?

🔑 2. Crittografia: Simmetrica e Asimmetrica

🔐 Cos'è la Crittografia?

La crittografia (o cifratura) è il processo di trasformare dati leggibili (plaintext) in dati incomprensibili (ciphertext) usando un algoritmo e una chiave. A differenza dell'hashing, la crittografia è reversibile: con la chiave giusta, puoi decifrare i dati e tornare al plaintext.

🔄 Processo di Crittografia (Bidirezionale)
Plaintext
"Messaggio"
Cifratura
+ Chiave
Ciphertext
"aXf9Kq..."
Ciphertext
"aXf9Kq..."
Decifratura
+ Chiave
Plaintext
"Messaggio"

🔑 Crittografia Simmetrica

📖 Concetto

Nella crittografia simmetrica, la stessa chiave viene usata sia per cifrare che per decifrare i dati. È come avere una cassaforte con una chiave: usi la stessa chiave sia per chiuderla che per aprirla.

🔑 Crittografia Simmetrica - Una Sola Chiave
Alice
🔑
Chiave
Condivisa
Bob

Alice e Bob usano la STESSA chiave per cifrare e decifrare

✅ Vantaggi: Veloce, efficiente per grandi quantità di dati

❌ Svantaggi: Problema della distribuzione della chiave, stessa chiave per tutti

💻 Implementazione con Fernet (AES)

pip install cryptography
from cryptography.fernet import Fernet

class SymmetricEncryption:
    """Gestisce la crittografia simmetrica usando Fernet (AES-128)."""
    
    def __init__(self):
        self.key = None
        self.cipher = None
    
    def generate_key(self):
        """Genera una nuova chiave casuale."""
        self.key = Fernet.generate_key()
        self.cipher = Fernet(self.key)
        return self.key
    
    def encrypt(self, plaintext: str) -> bytes:
        """Cifra un testo."""
        if self.cipher is None:
            raise ValueError("Nessuna chiave caricata")
        return self.cipher.encrypt(plaintext.encode('utf-8'))
    
    def decrypt(self, ciphertext: bytes) -> str:
        """Decifra un testo."""
        if self.cipher is None:
            raise ValueError("Nessuna chiave caricata")
        return self.cipher.decrypt(ciphertext).decode('utf-8')


# Esempio di utilizzo
crypto = SymmetricEncryption()
key = crypto.generate_key()

# Cifrare un messaggio
messaggio = "Questo è un messaggio segreto! 🔒"
cifrato = crypto.encrypt(messaggio)
print(f"Messaggio cifrato: {cifrato}")

# Decifrare il messaggio
decifrato = crypto.decrypt(cifrato)
print(f"Messaggio decifrato: {decifrato}")

🔐 Crittografia Asimmetrica (RSA)

📖 Concetto

Nella crittografia asimmetrica, si usano due chiavi diverse: una chiave pubblica per cifrare e una chiave privata per decifrare. È come una cassetta postale: chiunque può inserire una lettera (cifrare con la chiave pubblica), ma solo il proprietario può aprirla e leggerla (decifrare con la chiave privata).

🔐 Crittografia Asimmetrica - Due Chiavi
Alice
🔓
Chiave
Pubblica
di Bob
Messaggio
Cifrato
Bob

Bob decifra con la sua 🔑 Chiave Privata (segreta)

✅ Vantaggi: Non serve scambiare chiavi segrete, ogni utente ha la sua coppia

❌ Svantaggi: Molto più lento della simmetrica, limitato nella dimensione dei dati

🔑 Simmetrica (AES)

  • ✅ Veloce
  • ✅ Ideale per grandi file
  • ❌ Problema distribuzione chiave
  • ❌ Stessa chiave per tutti

Usa per: Cifrare file, database, backup

🔐 Asimmetrica (RSA)

  • ✅ Scambio chiavi sicuro
  • ✅ Firma digitale
  • ❌ Molto più lenta
  • ❌ Limitata nella dimensione

Usa per: Firme, certificati, scambio chiavi

📝 Quiz 2: Crittografia

Quale affermazione sulla crittografia asimmetrica è CORRETTA?

💉 3. SQL Injection - Prevenzione

⚠️ Cos'è SQL Injection?

SQL Injection è una delle vulnerabilità più pericolose. Permette a un attaccante di inserire codice SQL malevolo in una query, potenzialmente ottenendo accesso completo al database, rubando dati, modificandoli o cancellandoli.

Nel 2021, SQL Injection era al 3° posto nella OWASP Top 10 (le 10 vulnerabilità web più critiche).

❌ Esempio di Codice Vulnerabile

# ❌ VULNERABILE - NON FARE MAI QUESTO!
import sqlite3

def login_vulnerable(username, password):
    """Funzione di login VULNERABILE a SQL Injection."""
    conn = sqlite3.connect('users.db')
    cursor = conn.cursor()
    
    # ⚠️ PERICOLOSO: Query costruita con concatenazione di stringhe
    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    
    cursor.execute(query)
    result = cursor.fetchone()
    conn.close()
    
    return result is not None

# 🔓 ATTACCO SQL INJECTION
username_attacker = "admin' OR '1'='1"
password_attacker = "qualsiasi"

if login_vulnerable(username_attacker, password_attacker):
    print("💀 ATTACCO RIUSCITO! Login senza password!")

# La query diventa:
# SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'qualsiasi'
# Che è sempre vera perché '1'='1' è sempre vero!
💀 Come Funziona un Attacco SQL Injection
Input Normale
username = "mario"
password = "password123"
Query SQL
SELECT * FROM users WHERE username = 'mario' AND password = 'password123'
✅ Login OK
(se credenziali corrette)
Input Malevolo
username = "admin' OR '1'='1"
password = "qualsiasi"
Query SQL Iniettata
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'qualsiasi'
💀 Login BYPASSATO
(sempre vero!)

✅ Prevenzione: Query Parametrizzate

🛡️ La Soluzione: Query Parametrizzate

Le query parametrizzate (o prepared statements) sono il modo corretto e sicuro di eseguire query SQL. Invece di concatenare stringhe, si usano placeholder che vengono sostituiti in modo sicuro dal database.

import sqlite3

def login_secure(username, password):
    """Login SICURO usando query parametrizzate."""
    conn = sqlite3.connect('users.db')
    cursor = conn.cursor()
    
    # ✅ SICURO: Usa placeholder (?) invece di concatenazione
    # Il database gestisce automaticamente l'escape dei caratteri speciali
    query = "SELECT * FROM users WHERE username = ?"
    
    cursor.execute(query, (username,))  # Tupla di parametri
    result = cursor.fetchone()
    conn.close()
    
    return result is not None

# Tentativo di SQL Injection (fallisce!)
username_attacker = "admin' OR '1'='1"

if login_secure(username_attacker, "qualsiasi"):
    print("Login riuscito")
else:
    print("✅ Attacco bloccato! SQL Injection non funziona")
    # Username "admin' OR '1'='1" viene trattato come stringa letterale

✅ Best Practices Anti-SQL Injection

  1. SEMPRE usare query parametrizzate - Mai concatenare stringhe in SQL
  2. Usare ORM (SQLAlchemy, Django ORM) - Gestiscono automaticamente la sicurezza
  3. Validare input - Controlli aggiuntivi lato applicazione
  4. Principio del minimo privilegio - L'utente DB deve avere solo permessi necessari
  5. Web Application Firewall (WAF) - Filtri a livello di rete
  6. Auditing e logging - Monitora query sospette

📝 Quiz 3: SQL Injection

Quale tecnica è la migliore per prevenire SQL Injection?

✅ 4. Input Validation e Sanitization

📝 Cos'è la Validazione dell'Input?

La validazione dell'input è il processo di verificare che i dati forniti dagli utenti siano corretti, sicuri e conformi alle aspettative. È come un controllo di sicurezza all'aeroporto: tutto ciò che entra deve essere ispezionato.

Perché è fondamentale:

  • Previene SQL Injection, XSS e altri attacchi
  • Garantisce integrità dei dati
  • Migliora user experience (feedback immediato su errori)
  • Previene crash e comportamenti imprevisti

⚖️ Validazione vs Sanitization

Aspetto Validazione Sanitization
Definizione Verifica se l'input è valido Pulisce/modifica l'input per renderlo sicuro
Azione Accetta o rifiuta Trasforma
Esempio Verifica che email contenga @ Rimuove tag HTML da input
Quando Prima dell'elaborazione Prima del salvataggio/output

Best Practice: Usa ENTRAMBE! Prima valida, poi sanitizza se necessario.

💻 Esempio di Validazione Input

import re

class InputValidator:
    """Classe per validare vari tipi di input."""
    
    @staticmethod
    def validate_email(email):
        """Valida un indirizzo email."""
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        
        if not email:
            return False, "Email non può essere vuota"
        
        if len(email) > 254:
            return False, "Email troppo lunga"
        
        if not re.match(pattern, email):
            return False, "Formato email non valido"
        
        return True, None
    
    @staticmethod
    def validate_password(password):
        """Valida una password secondo criteri di sicurezza."""
        errors = []
        
        if len(password) < 8:
            errors.append("Password deve essere lunga almeno 8 caratteri")
        
        if not re.search(r'[A-Z]', password):
            errors.append("Password deve contenere almeno una maiuscola")
        
        if not re.search(r'[a-z]', password):
            errors.append("Password deve contenere almeno una minuscola")
        
        if not re.search(r'\d', password):
            errors.append("Password deve contenere almeno un numero")
        
        if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
            errors.append("Password deve contenere almeno un carattere speciale")
        
        return len(errors) == 0, errors


# Esempio di utilizzo
validator = InputValidator()

# Test Email
emails = ["user@example.com", "invalid.email", "user@"]

for email in emails:
    is_valid, error = validator.validate_email(email)
    status = "✅" if is_valid else "❌"
    print(f"{status} {email}: {error or 'Valida'}")

# Test Password
passwords = ["Abc123!@", "short", "nouppercase123!"]

for pwd in passwords:
    is_valid, errors = validator.validate_password(pwd)
    status = "✅" if is_valid else "❌"
    print(f"{status} {pwd}")
    if errors:
        for error in errors:
            print(f"   - {error}")

✅ Best Practices per Input Validation

  1. Valida lato server - Mai fidarsi del client
  2. Whitelist > Blacklist - Definisci cosa è permesso, non cosa è vietato
  3. Valida tipo, lunghezza, formato - Controlli multipli
  4. Feedback chiari - Spiega all'utente cosa è sbagliato
  5. Sanitizza sempre l'output - Specialmente per HTML
  6. Usa librerie consolidate - Non reinventare la ruota

📝 Quiz 4: Input Validation

Perché è importante validare l'input lato server e non solo lato client?

🌐 5. HTTPS e Certificati SSL/TLS

🔒 Cos'è HTTPS?

HTTPS (HTTP Secure) è la versione sicura di HTTP. Utilizza SSL/TLS (Transport Layer Security) per cifrare la comunicazione tra client e server, proteggendo i dati da intercettazione e manomissione.

🔓 HTTP vs 🔒 HTTPS

❌ HTTP (Non Sicuro)

Client → [dati in chiaro] → Server

🔓 Chiunque può leggere!

  • Password visibili
  • Dati intercettabili
  • Manipolazione possibile

✅ HTTPS (Sicuro)

Client → [dati cifrati] → Server

🔒 Solo client e server possono leggere

  • Dati cifrati
  • Integrità garantita
  • Autenticità verificata

⚠️ Perché Serve HTTPS?

  • 🔐 Confidenzialità: I dati sono cifrati e non possono essere letti da terzi
  • 🛡️ Integrità: I dati non possono essere modificati durante il trasferimento
  • ✅ Autenticità: Garantisce che stai comunicando con il server corretto
  • 📊 SEO: Google favorisce i siti HTTPS nel ranking
  • ⚖️ Compliance: Richiesto da GDPR e altre normative
  • 👥 Fiducia utenti: I browser mostrano lucchetto verde

🔑 Come Funziona SSL/TLS

🤝 TLS Handshake - Processo di Connessione Sicura
1. Client Hello
"Ciao Server! Supporto TLS 1.3"
2. Server Hello
"Ok! Ecco il mio certificato 📜"
3. Verifica Certificato
Client verifica certificato ✅
4. Scambio Chiavi
Generazione chiave simmetrica 🔑
5. Comunicazione Cifrata
🔐 Tutto il traffico è cifrato! 🔐

📜 Certificati SSL/TLS

🎫 Cos'è un Certificato?

Un certificato SSL/TLS è un documento digitale che:

  • Contiene la chiave pubblica del server
  • Include informazioni sul proprietario del dominio
  • È firmato da una Certificate Authority (CA) fidata
  • Ha una data di scadenza
Tipo Validazione Uso
DV (Domain Validation) Solo dominio Blog, siti personali
OV (Organization Validation) Dominio + Organizzazione Aziende, e-commerce
EV (Extended Validation) Verifica estesa Banche, finanza

💻 Implementare HTTPS

🚀 Let's Encrypt (Gratis!)

Let's Encrypt è una CA gratuita che fornisce certificati SSL/TLS automatici. È la scelta migliore per la maggior parte delle applicazioni web.

# Installare Certbot (client Let's Encrypt) sudo apt-get update sudo apt-get install certbot python3-certbot-nginx # Ottenere un certificato per il tuo dominio sudo certbot --nginx -d example.com -d www.example.com # Il certificato si rinnoverà automaticamente!

✅ Best Practices HTTPS

  1. Usa SEMPRE HTTPS - Mai HTTP per siti in produzione
  2. Forza HTTPS - Redirect automatico da HTTP a HTTPS
  3. HSTS - HTTP Strict Transport Security header
  4. TLS 1.3 - Usa la versione più recente di TLS
  5. Certificati validi - Usa Let's Encrypt o CA fidate
  6. Rinnovo automatico - Certbot gestisce i rinnovi
  7. Mixed Content - Tutte le risorse devono essere HTTPS

📝 Quiz 5: HTTPS e SSL/TLS

Quale delle seguenti NON è una caratteristica di HTTPS?

📝 Riepilogo e Checklist Sicurezza

✅ Checklist di Sicurezza per le Tue Applicazioni

🔐 Password e Autenticazione

  • ☐ Usa bcrypt (o argon2) per hashare le password
  • ☐ Mai salvare password in chiaro
  • ☐ Implementa blocco account dopo N tentativi falliti
  • ☐ Richiedi password forti (8+ caratteri, mix completo)
  • ☐ Implementa 2FA dove possibile

🔑 Crittografia

  • ☐ Usa crittografia simmetrica (AES) per dati at rest
  • ☐ Usa RSA per firme digitali e scambio chiavi
  • ☐ Non inventare algoritmi custom di crittografia
  • ☐ Gestisci le chiavi in modo sicuro (KMS)

💉 Database e SQL Injection

  • ☐ SEMPRE usa query parametrizzate
  • ☐ Preferisci ORM (SQLAlchemy, Django ORM)
  • ☐ Principio del minimo privilegio per utenti DB
  • ☐ Implementa logging di query sospette

✅ Input Validation

  • ☐ Valida lato server (mai fidarsi del client)
  • ☐ Usa whitelist invece di blacklist
  • ☐ Valida tipo, lunghezza, formato di ogni input
  • ☐ Sanitizza HTML per prevenire XSS
  • ☐ Rate limiting per prevenire abusi

🌐 HTTPS e Comunicazioni

  • ☐ Usa SEMPRE HTTPS in produzione
  • ☐ Forza redirect da HTTP a HTTPS
  • ☐ Implementa HSTS header
  • ☐ Usa TLS 1.2 o superiore
  • ☐ Certificati da CA fidate (Let's Encrypt)
  • ☐ Nessun mixed content